﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.Diagnostics;


using P2PLib.Common.DataModel;
using P2PLib.Client.Command;
using P2PLib.Client.DataModel;

namespace P2PLib.Client
{
    public class CClientManager
    {
        private Socket clientSocket;
        private ClientInfo client;
        private NetworkStream stream;
        private BackgroundWorker bgReceiver;
        private List<FileServer> fileServers;

        public event SearchEventHandler Search;
        public event ClientDisconnectedEventHandler ClientDisconnected;
        public event RequestFileEventHandler RequestFile;
        public event RequestCurrentPositionEventHandler RequestCurrentPosition;
        public event StartSendingEventHandler StartSending;

        #region property
        public bool Connected
        {
            get
            {
                return clientSocket.Connected;
            }

        }
        public IPAddress IP
        {
            get
            {
                return ((IPEndPoint)this.clientSocket.RemoteEndPoint).Address;
            }
        }
        public int Port
        {
            get
            {
                return ((IPEndPoint)this.clientSocket.RemoteEndPoint).Port;
            }
        }
        public ClientInfo Client
        {
            get
            {
                return this.client;
            }
        }

        public NetworkStream NetStream
        {
            get
            {
                return stream;
            }
        }
        #endregion //property

        public CClientManager(Socket soc,List<FileServer> fileServers)
        {
            this.clientSocket= soc;
            this.stream = new NetworkStream(soc);
            this.fileServers = fileServers;

            this.client = new ClientInfo(this.IP.ToString(), 1234);

            Console.WriteLine("Client {0} connected...",((IPEndPoint)soc.RemoteEndPoint).Address.ToString());
            bgReceiver = new BackgroundWorker();
            bgReceiver.DoWork += new DoWorkEventHandler(bgReceiver_DoWork);
            bgReceiver.WorkerSupportsCancellation = true;
            bgReceiver.RunWorkerAsync();

        }


        void bgReceiver_DoWork(object sender, DoWorkEventArgs e)
        {
            Console.WriteLine("Start recieving command from client");
            try
            {

                while (this.clientSocket.Connected)
                {
                    byte[] buffer = new byte[4];
                    int readBytes = stream.Read(buffer, 0, 4);
                    if (readBytes == 0)
                        break;
                    CCCommandType type = (CCCommandType)BitConverter.ToInt32(buffer, 0);

                    Console.WriteLine("Client Manager recived command, type:{0}", type);

                    switch (type)
                    {
                        case CCCommandType.Search:
                            //调用client相应地搜索方法
                            CCSearchCommand searchCmd = new CCSearchCommand();
                            searchCmd.ReadFromStream(stream);
                            if (this.Search != null)
                                Search(this, new SearchEventArgs(searchCmd.SearchType, searchCmd.Key));
                            break;


                        case CCCommandType.RequestFile:
                            CCRequestFileCommand fileCmd = new CCRequestFileCommand();
                            fileCmd.ReadFromStream(stream);
                            if (this.RequestFile != null)
                            {
                                //从本地查找该文件的信息;
                                LocalFileInfo fileInfo = RequestFile(this, new RequesFileEventArgs(fileCmd.FileHashCode));

                                if (fileInfo != null)
                                {
                                    //文件存在，则检测该文件的FileServer是否已经启动
                                    FileServer fs = this.GetFileServer(fileInfo);
                                    if (fs == null)
                                    {
                                        //启动文件服务器
                                        fs = new FileServer(fileInfo, GetRandomPort());
                                        fs.StartServer();
                                    }
                                    //告知远程客户端文件服务器的端口号;
                                    CCFileReadyCommand frc = new CCFileReadyCommand();
                                    stream.Write(BitConverter.GetBytes((int)frc.Type), 0, 4);

                                    frc.Port = fs.Port;
                                    frc.WriteToStream(this.stream);
                                }
                                else
                                {
                                    //告知远程客户端文件不存在;
                                    CCFileNotExistCommand fnec = new CCFileNotExistCommand();
                                    fnec.WriteToStream(this.stream);
                                }
                            }
                            else
                            {
                                Debug.WriteLine("CClientManager: no request event handler.");
                            }

                            break;
                        case CCCommandType.Disconnect:

                            //客户端断开连接
                            if (this.ClientDisconnected != null)
                                ClientDisconnected(this, new ClientEventArgs(new ClientInfo("127.0.0.1", 1234)));

                            break;

                        case CCCommandType.StopTransfer:
                            //调用客户端相应的方法，停止传输
                            break;
                        case CCCommandType.FileStartPosition:
                            //客户端已经链接到了文件服务器;
                            CCFileStaratPositionCommand fspc = new CCFileStaratPositionCommand();
                            fspc.ReadFromStream(stream);

                            //get the client's location in the download queue
                            if (this.RequestCurrentPosition != null)
                            {
                                int position = RequestCurrentPosition(this, new ClientEventArgs(new ClientInfo(this.IP.ToString(), 1234)));
                                if (position < FileServer.Limit)
                                {
                                    if (this.StartSending != null)
                                    {
                                        //stop reciveing command from remote client
                                        this.bgReceiver.CancelAsync();
                                        CCStartRecievingCommand src = new CCStartRecievingCommand();
                                        //tell the remote client to start recive file
                                        stream.Write(BitConverter.GetBytes((int)src.Type), 0, 4);

                                        //invoke FileServer's start sending file method
                                        StartSending(this, new SendingFileEventArgs(fspc.StartPostion));
                                    }
                                }
                                else
                                {
                                    CCCurrentPositionCommand cpc = new CCCurrentPositionCommand(position);
                                    this.SendCommand(cpc);
                                }
                            }
                            break;

                    }
                }
            }
            catch(Exception ex)
            {
                Debug.WriteLine("CClientManager:"+ex.Message+Environment.NewLine+ex.StackTrace);
            }
        }

        public void SendCommand(CCCommandBase command)
        {
            BackgroundWorker bgSender = new BackgroundWorker();
            bgSender.DoWork += new DoWorkEventHandler(bgSender_DoWork);
            bgSender.RunWorkerAsync(command);
        }

        //This Semaphor is to protect the critical section from concurrent access of sender threads.
        System.Threading.Semaphore semaphor = new System.Threading.Semaphore(1, 1);
        void bgSender_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                semaphor.WaitOne();
                CCCommandBase command = (CCCommandBase)e.Argument;
                int type = (int)command.Type;
                byte[] buffer = BitConverter.GetBytes(type);
                stream.Write(buffer, 0, 4);
                command.WriteToStream(stream);
                e.Result = true;
                semaphor.Release();
            }
            catch
            {
                e.Result = false;
            }
        }


        /// <summary>
        /// 获得文件服务器，如果不存在则返回空
        /// </summary>
        /// <param name="f"></param>
        /// <returns></returns>
        private FileServer GetFileServer(LocalFileInfo f)
        {
            foreach (FileServer fs in fileServers)
            {
                if(fs.File.CFile.FileHashCode.Equals(f.CFile.FileHashCode))
                    return fs;
            }
            return null;
        }

        /// <summary>
        /// 获得一个可用的随机端口号
        /// </summary>
        /// <returns></returns>
        private int GetRandomPort()
        {
            Random r = new Random();
            int port = 0;
            while (true)
            {
                 port = r.Next(5000, 60000);
                 if (!IsPortUsed(port))
                 {
                     break;
                 }
            }
            return port;
        }

        /// <summary>
        /// check if port has been used
        /// </summary>
        /// <param name="port"></param>
        /// <returns></returns>
        private bool IsPortUsed(int port)
        {
            foreach (FileServer fs in fileServers)
            {
                if (fs.Port == port)
                    return true;
            }
            return false;
        }
    }
}
